#!/usr/bin/env python


# import common stuff
from songanalysiscommon import *
from Playlist import Playlist

#defines

n_date_analyzed = "user.songanalysis.dateAnalyzed" # a number which should be the stat() mtime entry
n_tempo = "user.songanalysis.tempo" # a number, expressed in python str(float(x)) format.  if nil, or inf or -inf, it should NOT BE STORED
n_songanalysis_interface = "user.songanalysis.interfaceVersion" # a number determining the interface version
n_freqbands = "user.songanalysis.frequencyBands" # a series of float numbers separated by commas.  if not available, it should NOT BE STORED
n_voldiff = "user.songanalysis.volumeDifference" # a float number.  if not available, it should NOT BE STORED

#end defines

from threading import Thread,RLock,Lock
from Queue import Empty
import os
import sys
import signal
import time
import errno
import logging
from sets import Set

# runtime tests
try:
	from extattr import getfattr,setfattr
except:
	error = "The Smart DJ plug-in requires the python-extattr package to work properly.  Please install it and try again."
	say_sorry(error)
	sys.exit(0)
	
try:
	from commandsplus import shell_escape,getstatusoutputerror
except:
	error = "The Smart DJ plug-in requires the python-commandsplus package to work properly.  Please install it and try again."
	say_sorry(error)
	sys.exit(0)
	
#librarize this version class FIXME
class Version:
	major = 0
	minor = 0
	micro = 0
	def __init__(self,version_string):
		splitted = version_string.split(".",2)
		try:
			major = int(splitted[0])
			try: minor = int(splitted[1])
			except IndexError: minor = 0
			try: micro = int(splitted[2])
			except IndexError: micro = 0
		except ValueError: raise ValueError, "invalid version string for Version(): %s"%version_string
		self.major = major
		self.minor = minor
		self.micro = micro
		self.version_string = version_string
		
	def __cmp__(self,other):
		for versiontuple in [ (self.major,other.major),(self.minor,other.minor),(self.micro,other.micro) ]:
			mine,others = versiontuple
			if mine > others: return 1
			if mine < others: return -1
		return 0

def test_songanalysis():
	# the songanalysis should spit its version on stdout
	minversion = "0.2.1"
	status,output,error = getstatusoutputerror("songanalysis -V")
	if status.command_not_found:
		return "The Smart DJ plug-in requires the songanalysis program, which has not been installed.  Please ensure songanalysis is correctly installed and in your PATH."
	if status.exited and status.return_value == 70:
		return "An old version of the songanalysis program is installed.  The Smart DJ plug-in requires at least version %s."%minversion
	if status.exited and status.return_value == 0:
		current = Version(output.strip())
		required = Version(minversion)
		if current >= required:
			return None # this means OKAY
		return "An old version of the songanalysis program is installed.  The Smart DJ plug-in requires at least version %s."%minversion
	return "An error ocurred when checking for the songanalysis program (%s).  Please ensure songanalysis is correctly installed and in your PATH."%(status)

ret = test_songanalysis()
if ret:
	say_sorry(ret)
	sys.exit(0)


try:
	import songanalysisui
	ui_available = True
except ImportError: ui_available = False



# utility functions

def sql_escape(string):
	newstring = []
	for c in string:
		if c == "\\":
			newstring.append("\\\\")
		elif c == "'":
			newstring.append("\\'")
		else:
			newstring.append(c)
	return "'" + "".join(newstring) + "'"

def setup_logging():
# 	logging.basicConfig()
	f = file(os.path.expanduser(n_logfile),"w",0)
	handler = logging.StreamHandler(f)
	format = "%(levelname)s:%(name)s: %(message)s"
	formatter = logging.Formatter(format)
	handler.setFormatter(formatter)
	logging.getLogger().addHandler(handler)
	logging.getLogger().setLevel(logging.DEBUG)
	
# 	logger.debug("sys.argv: %s",sys.argv)
	if len(sys.argv) > 1 and sys.argv[1] == "-v":
		handler = logging.StreamHandler()
		handler.setFormatter(formatter)
		logging.getLogger().addHandler(handler)
	else:
		sys.stdout = f
		sys.stderr = f
# 		def log_exception(thetype,thevalue,traceback):
# 			print "MIERDA NOS HIZIMOS VERGA"
# 			from traceback import format_exception
# 			text = format_exception(  thetype, thevalue, traceback)
# 			logger.exception("Unhandled exception")
# 		sys.excepthook = log_exception
# 		sys.stdout = file("/dev/null","w")
# 		sys.stderr = file("/dev/null","w")
	
def finish_logging():
	logging.shutdown()

logger = logging.getLogger()

def passive_popup(message,title="Smart DJ",timeout=5):
	assert(type(title) is str)
	AsyncProcess(target=run,args=('kdialog','--title',title,'--passivepopup',message,str(timeout))).start()
	
def get_available_songanalysis_interface():
	r = getstatusoutputerror(["songanalysis","-N"])
	if r[0].exited and r[0].return_value == 0: return r[1].strip()
	raise Exception,str(r[0])

def dcop_amarok(object_name,*args):
# 	args = [ shell_escape(a) for a in args ]
	command = ["dcop","amarok",object_name]
	for arg in args: command.append(arg)
# 	 %s %s"%(shell_escape(object_name)," ".join(args))
# 	if not "COALESCE" in command:
# 		logger.debug("running command %s"%command)
	status,output,error=getstatusoutputerror(command)
	if not (status.exited and status.return_value == 0):
		raise DcopAmarokFailed(command,status,output+error)
	return output.strip()

def playlist_popup(message):
	dcop_amarok("playlist","popupMessage",message)

def short_status_message(message):
	dcop_amarok("playlist","shortStatusMessage",message)
	
# end utility functions


# classes

class SongObject(object):
	def __init__(self,path,collection_expert):
		self._exec_query = collection_expert._exec_query
		self.path = path
		
	def __get_artist(self):
		if not hasattr(self,"_artist"):
			self._artist = self._exec_query("select artist.name from artist inner join tags on tags.artist = artist.id where tags.url = %s;"%sql_escape(self.path))
		return self._artist
	
	def __get_album(self):
		if not hasattr(self,"_album"):
			self._album = self._exec_query("select album.name from album inner join tags on tags.album = album.id where tags.url = %s;"%sql_escape(self.path))
		return self._album
	
	def __get_title(self):
		if not hasattr(self,"_title"):
			self._title = self._exec_query("select title from tags where tags.url = %s;"%sql_escape(self.path))
		return self._title
	
	artist = property(__get_artist)
	title = property(__get_title)
	album = property(__get_album)


class CollectionExpert:
		
	logger = logging.getLogger("CollectionExpert")
	
	def __init__(self):
		self.analysis_priority_queue = []
		
	def add_to_priority_queue(self,filename): # we need to serialize this around a common lock X
		if not filename in self.analysis_priority_queue:
			self.logger.debug("Adding to priority queue: %s",filename)
			self.analysis_priority_queue.append(filename)
		
	def get_priority_queue_count(self):
		return len(self.analysis_priority_queue)
		
	def _exec_query(self,sql):
# 		self.logger.debug("Executing SQL: %s",sql)
		return dcop_amarok("collection","query",sql)
		
	def purge_songs_not_analyzed_with_interface(self,interface):
		sql1 = "delete from analysis where interface_used is NULL;"
		sql2 = "delete from analysis where interface_used != %s;"%sql_escape(interface)
		for a in [sql1,sql2]: self._exec_query(a)
		
	def get(self): # we need to serialize this around a common lock X
		try:
			pqe = self.analysis_priority_queue[0]
# 			self.logger.debug("Returning from priority queue: %s",pqe)
			return pqe
		except IndexError:
			sql = "select tags.url from tags left join analysis on (tags.url = analysis.url) where analysis.url is NULL limit 1;"
			# or analysis.interface_used != %s or analysis.interface_used is NULL limit 1
			response = self._exec_query(sql)
			if not response: raise Empty
			else: return response
	
	def get_current_song(self):
		try: return self.get()
		except Empty: return None
		
	def get_total_songs(self):
		sql = "select count(url) from tags as totalcount;"
		response = self._exec_query(sql)
		return int(response)
	
	def get_remaining_songs(self):
		sql = "select count(tags.url) as remaining from tags left join analysis on (tags.url = analysis.url) where analysis.url is NULL;"
		response = self._exec_query(sql)
		return int(response)
	
	def _delete_file(self,filename):
		sql = "delete from analysis where url = %s;"%sql_escape(filename)
		self._exec_query(sql)
	
	def associate(self,filename,tempo,freqbands,voldiff,songanalysis_interface_installed):
		
		if filename in self.analysis_priority_queue:
			self.logger.debug("Removing from priority queue: %s",filename)
			self.analysis_priority_queue.remove(filename)
			
		self._delete_file(filename)
# 		
		if tempo is None: tempo = "NULL"
		else: tempo = "%f"%tempo
		if voldiff is None: voldiff = "NULL"
		else: voldiff = "%f"%voldiff
		if freqbands is None: freqbands = ",".join([ "NULL" for r in range(30) ])
		else: freqbands = ",".join([ "%f"%f for f in freqbands ])
		
		fnames = ",".join([ "freq%s"%s for s in range(30) ])
		sql = "insert into analysis (url,volume_diff,%s,bpm,interface_used) values(%s,%s,%s,%s,%s);" %(fnames,sql_escape(filename),voldiff,freqbands,tempo,sql_escape(songanalysis_interface_installed))
		
		self._exec_query(sql)
		
	def get_song(self,path):
		
		return SongObject(path,self)
	
	def get_similar_songs(self,filename,weight_freq,weight_bpm,limit):
		sql = "select * from analysis where url = %s;"% sql_escape(filename)
		output = self._exec_query(sql).splitlines()
		if not output:
			raise NotAnalyzedYet,filename
		if len(output) != 34:
			raise UnAnalyzable,"%s did not produce 34 output fields"%filename
		
		url,volume_diff,freqbands,bpm = output[0],float(output[1]),[ float(a) for a in output[2:32] ],float(output[32])
		
		def gen_volume_diff_sql (volume_diff):
			# return the relative difference in volume (positive means louder than the passed volume, negative means quieter)
			if volume_diff is None: volume_diff = "NULL"
			template = "volume_diff - %s"
			sql = template % (volume_diff)
			# we return unadjusted values
			return sql
		def gen_norm_volume_diff_sql (volume_diff):
			# normalized volume value, -1 ... 1, 1 has most similar volume
			s = gen_volume_diff_sql(volume_diff)
			s = "GREATEST( -1 , 1 - ABS( %s ) )"%s
			return s
		def gen_band_interference_sql(freqbands):
			# return the magnitude of the compound interference between frequency bands.  the larger the value, the most interference, the less compatible to the freqbands val passed.
		
			portions = []
			freq = freqbands
			
			band = 0
			template = "abs(%s - freq%s) + abs(%s - freq%s)"
			portions.append(template%(freq[band],band,freq[band],band+1))
			
			for band in range(1,29):
				template = "abs(%s - freq%s) + abs(%s - freq%s) + abs(%s - freq%s)"
				portions.append(template%(freq[band],band,freq[band],band-1,freq[band],band+1))
			
			band = 29
			template = "abs(%s - freq%s) + abs(%s - freq%s)"
			portions.append(template%(freq[band],band,freq[band],band-1))
			
			sql = " + ".join(portions)
			# 	sql = "1 - (( %s ) / 2.5 )"%sql
			# we return unadjusted values
			
			return sql
		def gen_norm_band_interference_sql(freqbands):
	# 		normalized interference value: -1...1, 1 has most similar freq spectrum distribution
			s = gen_band_interference_sql(freqbands)
			s = "1 - ( ( %s ) / 2.5)" % s
			s = "LEAST( GREATEST( %s , -1 ) , 1 )" %s
			return s
		def gen_compatibility_sql(volume_diff,freqbands):
			# this returns -1 ... 1 ranged values which take norm vol and norm interf. and sum them in a weighted sum 3/4 for the interference and 1/4 for the vol difference
			f = gen_norm_band_interference_sql(freqbands)
			v = gen_norm_volume_diff_sql(volume_diff)
			s = "0.75 * ( %s ) + 0.25 * ( %s )"%(f,v)
			return s
		def gen_bpm_diff_sql(bpm):
			if bpm is None: bpm = "NULL"
			sql = "bpm - %s"%bpm
			return sql
		def gen_norm_bpm_diff_sql(bpm):
			s = gen_bpm_diff_sql(bpm)
			s = "ABS( %s ) / (160 - 120)"%s  #MAX_BPM MIN_BPM ... how the fuck do they choose it?
			s = "1 - ( ( %s ) * 2 )"%s
			return s
		def gen_attraction_sql(volume_diff,freqbands,bpm,weight_freq,weight_bpm):
			c = gen_compatibility_sql(volume_diff,freqbands)
			b = gen_norm_bpm_diff_sql(bpm)
			s = "COALESCE( %s * %s , 0 ) + COALESCE( %s * %s , 0 )"%(c,weight_freq,b,weight_bpm)
			# we coalesce so if any value comes null, we simply sum zero to it
			return s
		def gen_force_sql(volume_diff,freqbands,bpm,weight_freq,weight_bpm):
			s = gen_attraction_sql(volume_diff,freqbands,bpm,weight_freq,weight_bpm)
			s = "ABS( %s ) * ( %s )"%(s,s) #we want to keep the sign that's why there is only one abs
			return s
		
		sql = "select COALESCE( tags.url , 'ERROR' ) as url , COALESCE( %s , -1 ) as force_value, COALESCE( bpm , 0) as tempo from analysis inner join tags on analysis.url = tags.url where volume_diff is not NULL order by force_value desc limit %s;"
		sql = sql%(gen_force_sql(volume_diff,freqbands,bpm,weight_freq,weight_bpm),limit)
		
		output = self._exec_query(sql)
		output = output.splitlines()
		assert ( len(output) / 3 ) * 3 == len(output)
		urls = [] ; forces = [] ; bpms = []
		while output:
			a = output.pop(0)
			urls.append(a)
			a = output.pop(0)
			forces.append(a)
			a = output.pop(0)
			bpms.append(a)
		results = []
		while urls and forces and bpms:
			results.append (     (    urls.pop(0) , float(forces.pop(0)) ,    float(bpms.pop(0))  )      )
		return results
		





class NotSong(Exception): pass
class BackgroundSongAnalyzer(Thread):
	
	logger = logging.getLogger("SongAnalyzer")
	
	def __init__(self,collection_expert):
		Thread.__init__(self)
		self.collection_expert = collection_expert
		self.__finish = False
		
		
	def stop(self,async=False):
		self.__finish = True
		if not async:
			self.join()
	
	def run(self):
		
		try: self._run()
		except AmarokDcopFailed,e:
			if e.output == "output call failed":
				self.logger.debug("amaroK is unexpectedly gone")
			else: raise
		except:
			self.logger.exception("Something wrong in background song analyzer, thread stopped")
			say_sorry("Something went wrong in the analyzer thread loop.  This is not your fault, but a bug in the Smart DJ plug-in.  Please report the contents of the file %s to the Smart DJ plug-in developers." %os.path.expanduser(n_logfile))
		
	def _run(self):
		analyzing_popup_shown = False
		done_popup_shown = False
		while self.__finish is False: #with this, we make this thread finish
			try:
				next_song = self.collection_expert.get()
				if analyzing_popup_shown is False:
					short_status_message("Smart DJ is analyzing your collection")
					analyzing_popup_shown = True
					done_popup_shown = False # we want to show this once a change in your collection is found
				self.process_song(next_song)
			except Empty:
				if done_popup_shown is False:
					short_status_message("Your music collection has been fully analyzed")
					done_popup_shown = True
					analyzing_popup_shown = False # the same goes for this
				self.logger.debug("sleeping")
				for seconds in range(30):
					if self.__finish is False: time.sleep(1)
				
			#we should rather simply sleep until the main thread signals that a rescan of the collection has just finished
		self.logger.debug("Analyzer done")
			
	def process_song(self,file):
		self.logger.info("Processing: " + file)
		
		# we begin by assuming we're analyzing the file and using eas
		onlyCollect = False
		useExtattrs = True
		songanalysis_interface_installed = get_available_songanalysis_interface()
		# make something that if this value changes from function call N to function call M, the purge_songs_not_analyzed_with_interface function is called FIXME
		try: # Let's see if the file has songanalysis EAs
			date_analyzed  = getfattr(file,n_date_analyzed)
			
			#we'll also check if the EAs in the file were analyzed by the same songanalysis interface as the one we have installed
			try: songanalysis_interface_used  = getfattr(file,n_songanalysis_interface)
			except KeyError: songanalysis_interface_used = None
			self.logger.debug("Songanalysis interface available: %s     used for this song: %s"%(songanalysis_interface_installed,songanalysis_interface_used))
			
			if songanalysis_interface_used == songanalysis_interface_installed:
				# if the interfaces match, it's okay to collect from EAs
				self.logger.debug("A-OK, we only collect")
				onlyCollect = True # since the interfaces match and there is a date of analysis we will only collect
			else:
				# interface mismatch, a reanalysis is in order
				self.logger.debug("Interface mismatch, we will reanalyze")
				
		except KeyError: # File did not have an EA
			self.logger.debug("No last analysis date EA found, assuming no EA thus need to reanalyze and restore in the EA")
			
		except IOError, e:
				if e.errno == errno.EOPNOTSUPP: #No support for EAs on the filesystem where the file is installed
					onlyCollect = False # we will analyze the song
					useExtattrs = False # we won't store it on eas
				elif e.errno == errno.ENOENT:
					onlyCollect = False
					useExtattrs = False
					self.logger.debug("The song %s does not exist, letting the songanalysis process catch this so this file is marked as invalid"%file)
				else: raise
		
		if useExtattrs and onlyCollect: #if we're using already available eas
			self.logger.debug( "collecting info from extattrs")
			# we collect the info from the eas
			try: tempo = float(getfattr(file,n_tempo))
			except KeyError: tempo = None
			try:
				freqbands = getfattr(file,n_freqbands)
				freqbands = freqbands.split(",")
				freqbands = [ float(f) for f in freqbands ]
			except KeyError: freqbands = None
			try: voldiff = float(getfattr(file,n_voldiff))
			except KeyError: voldiff = None
			self.logger.debug( "collected %s %s %s"%(tempo,freqbands,voldiff))
		else:
			process = AsyncProcess(self.analyze_song,(file,))
			process.start()
			
			# and periodically check whether it is done
			while self.__finish is False: # we periodically check whether we should abandon this
				try:
					tempo,freqbands,voldiff=process.get(block=True,timeout=1)
					break
				except NotReadyYet:
					continue
				except NotSong:
					# this is not a song, but we will mark it as NULL NULL NULL
					tempo,freqbands,voldiff=(None,None,None)
					break
		
			if self.__finish:
				self.logger.debug("shutdown requested, abandoning ongoing analysis")
				return
			
			self.logger.debug( "deduced %s %s %s"%(tempo,freqbands,voldiff))
			
			if useExtattrs: # we store the info in eas
				try:
					if tempo is not None:
						setfattr(file,n_tempo,str(tempo))
					if freqbands is not None:
						setfattr(file,n_freqbands,",".join([str(f) for f in freqbands]))
					if voldiff is not None:
						setfattr(file,n_voldiff,str(voldiff))
					date_analyzed = os.stat(file).st_mtime
					setfattr(file,n_date_analyzed,str(date_analyzed))
					setfattr(file,n_songanalysis_interface,songanalysis_interface_installed)
				except IOError,e:
					if e.errno == errno.EACCES: self.logger.debug( "no permission to save EAs for file %s, ignoring and moving on"%(file))
					elif e.errno == errno.ENOENT: self.logger.warning( "file %s vanished during analysis, saving in database, ignoring EA saves and moving on"%(file))
					else: raise
			
		# finally, we store the info in the DB
		self.store_analysis_info(file,tempo,freqbands,voldiff,songanalysis_interface_installed)
		
	def analyze_song(self,file):
		# return (tempo,freqbands,voldiff) (float,list of floats,float)
		
		self.logger.debug( "analyzing %s"%file)
		
		cmd = ["nice" ,"-n" ,"20" ,"songanalysis" ,file]
		status,output,error=getstatusoutputerror(cmd)
		
		if not status.exited:
			raise Exception, "songanalysis failed with status %s msg %s"%(status,error)
		if status.return_value == 70:
			raise NotSong, "%s is not a song understood by songanalysis, cannot be opened for reading or does not exist"%file

# 		self.logger.debug("analyzer spit out %s"%output)
		lines = [ l.strip() for l in output.splitlines() if l.strip() ]
		tuples = []
		for l in lines:
			key,val = l.split(":",1)
			tuples.append( (key.strip(),val.strip()) )
		
		tempo = None
		voldiff = None
		freqbands = None
		
		def niinfninan(x):
			if str(x) != "inf" and str(x) != "nan": return True
			return False
			
		for t in tuples:
			key,val = t
			if key == "Volume diff":
				val = float(val)
				if niinfninan(val): voldiff = val
			if key == "Frequencies":
				val = [ float(v) for v in val.split(" ") ]
				if niinfninan(val[0]): freqbands = val
			if key == "BPM":
				val = float(val)
				if niinfninan(val): tempo = val

		return tempo,freqbands,voldiff #if any of these is unavailable
		# we simply return None in that case
	
	def store_analysis_info(self,file,tempo,freqbands,voldiff,songanalysis_interface_installed):
		self.collection_expert.associate(file,tempo,freqbands,voldiff,songanalysis_interface_installed)


class PlaylistExpert:
	logger = logging.getLogger("PlaylistExpert")
	name = "playlist"
	get_playlist_lock = Lock()
	
	def _dcop(self,*args):
		return dcop_amarok("playlist",*args)
	
	def getActiveIndex(self):
		return int(self._dcop("getActiveIndex"))
	
	def getPlayingPath(self):
		return dcop_amarok("player","path")
	
	def getTotalTrackCount(self):
		return int(self._dcop("getTotalTrackCount"))

	def addMedia(self,url):
		self.logger.debug("Adding %s to end of playlist",url)
		return self._dcop("addMedia",url)

	def _get_playlist_paths(self):
		self.logger.debug("Getting playlist")
		p = Playlist()
		return [ a.Path for a in p.tracks ]
	
	def get_playlist_paths(self):
		self.get_playlist_lock.acquire()
		try: return self._get_playlist_paths()
		finally: self.get_playlist_lock.release()


class SuperDynamicModeMonitor(Thread,Queue):
		
	logger = logging.getLogger("SuperDynamicModeMonitor")
	
	def __init__(self,playlist_expert,collection_expert):
		Thread.__init__(self)
		Queue.__init__(self)
		self.__finish = False
		self.playlist_expert = playlist_expert
		self.collection_expert = collection_expert
		self.already_processed_files = Set()
		self.failed_cache = Set()
		self.sdm_enabled = False
		self.loop_running = RLock()
		
	def stop(self,async=False):
		self.__finish = True
# 		self.notify("end")
		if not async:
			self.join()
	
	def run(self):
		try: self._run()
		except AmarokDcopFailed,e:
			if e.output == "output call failed":
				self.logger.debug("amaroK is unexpectedly gone")
			else: raise
		except:
			self.logger.exception("Something wrong in Auto DJ mode monitor, process stopped")
			say_sorry("Something went wrong in the Auto DJ monitor loop.  This is not your fault, but a bug in the Smart DJ plug-in.  Please report the contents of the file %s to the Smart DJ plug-in developers." %os.path.expanduser(n_logfile))
		
	def notify(self,event):
		self.put(event)
	
	def _run(self):
		self.on_start()
		while self.__finish is False: #with this, we make this process finish
			try: event = self.get(block=True,timeout=1)
			except Empty:
# 				self.logger.debug("No event received")
				continue
			self.logger.debug("Event received: %s",event)
			if event == "track_changed": self.on_track_changed()
			if event == "config_changed": self.on_config_changed()
			if isinstance(event,Exception): raise event
			
		self.on_end()
		self.logger.debug("Auto DJ Monitor bailed")

	def find_suitable_song(self,start_song,num_choices,spectrum_weight,tempo_weight,prune_songs):
		ce = self.collection_expert
		
		num_results = num_choices + 1 + len(prune_songs) + 10 # the +1 is because we know the start song will come first, and the +10 is because we'll prune songs with the same file name!
		
		self.logger.debug("Finding %s songs tempo/spectrum %s/%s",num_results,tempo_weight,spectrum_weight)
		results = ce.get_similar_songs(start_song,spectrum_weight,tempo_weight,num_results)
		results = [ r[0] for r in results ]
		
# 		prune_songs_cache = Set()
		if prune_songs:
			esequele = "select CONCAT( artist.name , ' - ' , tags.title ) from artist inner join tags on tags.artist = artist.id where tags.url in ( %s );" % " , ".join([ sql_escape(e) for e in prune_songs ])
			prune_songs_cache = [ s.lower() for s in ce._exec_query(esequele).splitlines() ]
		else:
			prune_songs_cache = []
			
	# 	self.logger.debug("Results: %s\n",results)
		self.logger.debug("We will be pruning: %s\n",prune_songs)
		self.logger.debug("We will also be pruning: %s\n",prune_songs_cache)
		
		nuevos_elementos = []
		for result in results:
			add = True
			if result in prune_songs:
				self.logger.debug("Pruning %s from results list",result)
				continue
			
# 			rs = ce.get_song(result)
			esequele = "select CONCAT( artist.name , ' - ' , tags.title ) from artist inner join tags on tags.artist = artist.id where tags.url = %s ;" % sql_escape(result)
			compare_string = ce._exec_query(esequele).lower()
			
			if compare_string in prune_songs_cache:
				self.logger.debug("Pruning %s from results list",compare_string)
				continue
				
			nuevos_elementos.append(result)
			if len(nuevos_elementos) >= num_choices:
				self.logger.debug("Our pruned results list has achieved %s elements, continuing",num_choices)
				break
			
		results = nuevos_elementos[0:num_choices]
	# 	self.logger.debug("Pruned results: %s",results)
		from random import shuffle
		shuffle(results)
		if results: return results[0]
		else: return None

	def run_super_dynamic_loop(self): # Super Dynamic Loop!!!! TA-DAA!!!!!
		
		
		def run_if_unlocked():
			if not self.loop_running.acquire(blocking=0):
				return
			try:self._the_real_run()
			finally:self.loop_running.release()
		
		def run_guarded():
			try:
				run_if_unlocked()
			except AmarokDcopFailed,e:
				if e.output == "output call failed":
					self.logger.debug("amaroK is unexpectedly gone (run_super_dynamic_loop)")
				else: raise
			except Exception,e:
				self.logger.exception("Error while running super dynamic loop")
				self.notify(e)

		t = Thread(target=run_guarded)
		t.setDaemon(True)
		t.start()
		
	def _the_real_run(self):
		
		def count_upcoming_songs():
			r = self.playlist_expert.getTotalTrackCount() - self.playlist_expert.getActiveIndex() - 1
			assert(r >= 0)
# 			self.logger.debug("Upcoming songs: %s",r)
			return r
		
		sdm = get_config()["super_dynamic_mode"]
		if sdm["enabled"] and count_upcoming_songs() < sdm["upcoming_songs"]:
			
			playlist = self.playlist_expert.get_playlist_paths()
			for path in playlist:
				if path: self.already_processed_files.add(path)
		
			if sdm["choice_mechanism"] in [SONG_IM_LISTENING,SONG_IM_LISTENING_WANDER]:
				current_url = self.playlist_expert.getPlayingPath()
			elif sdm["choice_mechanism"] == LAST_SONG_IN_PLAYLIST:
				if len(playlist): current_url = playlist[-1]
				else: current_url = None
			else: assert(False)
			
			if not current_url:
				self.logger.debug("No start song, bailing")
				return # nothing's playing so we cannot determine what to use as criteria for start_url
			
			#FIXME this is for debuging
			start_time_2 = time.time()
			while sdm["enabled"] and count_upcoming_songs() < sdm["upcoming_songs"]:
				
				start_time = time.time()
				current_song = self.collection_expert.get_song(current_url)

				self.logger.debug("Trying to find a similar song to %s",current_url)
				try:
					self.already_processed_files.add(current_url)
					choice = self.find_suitable_song(
						current_url,sdm["top_choices"],
						sdm["tempo_spectrum_weights"][1],sdm["tempo_spectrum_weights"][0],
						self.already_processed_files)
					if choice:
						self.logger.debug("Choice: %s",choice)
						self.playlist_expert.addMedia(choice)
						choice_song = self.collection_expert.get_song(choice)
						self.already_processed_files.add(choice)
						short_status_message("Auto DJ mode added %s - %s to the playlist"%(choice_song.artist,choice_song.title))
						if sdm["choice_mechanism"] in [SONG_IM_LISTENING_WANDER,
							LAST_SONG_IN_PLAYLIST]:
								current_url = choice # yes, sir!
					else: raise NoChoicesFound
				except NotAnalyzedYet:
					if current_url not in self.failed_cache:
						self.logger.warning("%s was not analyzed yet, adding to priority queue",current_url)
						self.collection_expert.add_to_priority_queue(current_url)
						short_status_message("Auto DJ mode is analyzing %s - %s" 
							%(current_song.artist,current_song.title))
						self.failed_cache.add(current_url)
					break
				except UnAnalyzable:
					if current_url not in self.failed_cache:
						self.logger.warning("%s could not be analyzed by songanalysis, breaking",current_url)
						self.collection_expert.add_to_priority_queue(current_url)
						short_status_message("Auto DJ mode was unable to analyze %s - %s" 
							%(current_song.artist,current_song.title))
						self.failed_cache.add(current_url)
					break
				except NoChoicesFound:
					if current_url not in self.failed_cache:
						self.logger.warning("%s did not turn up any choices, breaking loop",current_url)
						short_status_message("Auto DJ mode could not find a song similar to %s - %s."
							%(current_song.artist,current_song.title))
						self.failed_cache.add(current_url)
					break
				self.logger.debug("Loop took %s seconds",time.time() -start_time)
			self.logger.debug("Total run took %s seconds",time.time() -start_time_2)
	
	def on_track_changed(self):
		self.run_super_dynamic_loop()
		
	def preserve_and_disable_amarok_config(self):
		m = self
		m.random_mode_status = dcop_amarok("player","randomModeStatus")
		m.dynamic_mode_status = dcop_amarok("player","dynamicModeStatus")
		dcop_amarok("player","enableRandomMode","false")
		dcop_amarok("player","enableDynamicMode","false")
		
	def restore_amarok_config(self):
		m = self
		if hasattr(m,"random_mode_status"):
			dcop_amarok("player","enableRandomMode",m.random_mode_status)
		if hasattr(m,"dynamic_mode_status"):
			dcop_amarok("player","enableDynamicMode",m.dynamic_mode_status)
	
	def on_config_changed(self):
		self.logger.debug("Auto DJ Monitor detected config change or just started up, adjusting everything")
		
		if get_config()["super_dynamic_mode"]["enabled"]:
			if not self.sdm_enabled:
				self.preserve_and_disable_amarok_config()
				self.already_processed_files = Set()
				short_status_message("Auto DJ mode is now on.  Please do not enable Random or Dynamic modes.")
				self.sdm_enabled = True
				# we just got enabled
			self.run_super_dynamic_loop() # this needs to run whether we're just enabled or not, to correct any "imperfections" and adjust to the users' requests
		else:
			if self.sdm_enabled:
				# we just got disabled
				self.sdm_enabled = False
				self.restore_amarok_config()
				short_status_message("Auto DJ mode is now off.")
	
	def on_end(self):
		self.logger.debug("About to enter on_end")
		self.restore_amarok_config()
	
	def on_start(self):
		self.on_config_changed()
		
		def run_loop_every_five():
			while True:
				time.sleep(5)
				self.run_super_dynamic_loop()
		
		t = Thread(target=run_loop_every_five)
		t.setDaemon(True)
		t.start()


class AmarokSlaveMonitor(Thread):
		
	logger = logging.getLogger("SlaveMonitor")
	
	def __init__(self,playlist_expert,collection_expert,super_dynamic_mode_monitor):
		Thread.__init__(self)
		self.__finish = False
		self.playlist_expert = playlist_expert
		self.collection_expert = collection_expert
		self.super_dynamic_mode_monitor = super_dynamic_mode_monitor
		
	def stop(self,async=False):
		self.__finish = True
		if not async:
			self.join()
	
	def run(self):
		try: self._run()
		except AmarokDcopFailed,e:
			if e.output == "output call failed":
				self.logger.debug("amaroK is unexpectedly gone")
			else: raise
		except Exception,e:
			self.logger.exception("Something wrong in monitor, process stopped %s %s",e,e.__class__)
			say_sorry("Something went wrong in the monitor loop.  This is not your fault, but a bug in the Smart DJ plug-in.  Please report the contents of the file %s to the Smart DJ plug-in developers." %os.path.expanduser(n_logfile))
		except:
			self.logger.exception("Something wrong in monitor, process stopped")
			say_sorry("Something went wrong in the monitor loop.  This is not your fault, but a bug in the Smart DJ plug-in.  Please report the contents of the file %s to the Smart DJ plug-in developers." %os.path.expanduser(n_logfile))
		
	
	def _run(self):
		import select
		import urllib
		po = select.poll()
		po.register(sys.stdin,select.POLLIN|select.POLLHUP|select.POLLERR|select.POLLPRI)
		while self.__finish is False: #with this, we make this process finish
			
# 			logger.debug("Parent PID: %s",os.getppid())
			if os.getppid() == 1: # we were reaped by INIT, shit!, parent is gone, we have to go!
				self.logger.debug("Parent is gone, forced exit")
				break
			
			try: readyfds = po.poll(1000) # wait one second
			except select.error,e:
				if e.args[0] == 4: continue # we were stopped, and now we're cont'ed
				else: raise
			if not readyfds: continue # spin because nothing good came out of this wait instance
				
			# or, if something good came out of it, read the line
			self.logger.debug("A new message from amaroK is in the queue")
# 			line = sys.stdin.readline().strip()
			try: line = sys.stdin.readline().strip()
			except IOError,e:
				if e.errno == 5:
					self.logger.debug("IOError 5, forced exit")
					break
				elif e.errno == 4: continue # we were stopped, and now we're cont'ed
				else: raise
			if line:
				# amarok sent us an event, here we do something useful
				self.logger.debug( "Processing: %s"%line)
				s = "customMenuClicked: Smart DJ Find songs similar to selection"
				t = "customMenuClicked: Smart DJ Set up Auto DJ mode"
				u = "customMenuClicked: Smart DJ Analyze selection now"
				if line[:len(s)] == s:
					fnames = line[len(s)+1:]
					fnames = [ urllib.url2pathname(fname)[7:] for fname in fnames.split(" ") ]
					songanalysisui.search(fnames)
				elif line[:len(u)] == u:
					fnames = line[len(u)+1:]
					fnames = [ urllib.url2pathname(fname)[7:] for fname in fnames.split(" ") ]
					for fname in fnames:
						self.collection_expert.add_to_priority_queue(fname)
					if len(fnames) == 1: mm = ""
					else: mm = "s"
					short_status_message("Added %s song%s to analysis priority queue"%(len(fnames),mm))
				elif line[:len(t)] == t or line == "configure":
					if ui_available:
						songanalysisui.show_configuration()
					else:
						say_sorry("To monitor analysis progress, you must have a working installation of the GTK+ library and the PyGTK bindings.  Work is under way to provide a Qt/KDE user interface.")
				elif line == "trackChange":
					# track changed
					# if Auto DJ is enabled:
					# basically, if after the track i'm listening there are fewer tracks than what the user requested with the configuration for Auto DJ, then we should add as many tracks as the user requested
					if get_config()["super_dynamic_mode"]["enabled"]:
						self.super_dynamic_mode_monitor.notify("track_changed")
							
			else:
				# amarok sent us no event, probably because we received
				# a signal, so we just bail out of the loop
				self.logger.debug( "Empty message, bailing")
				break
		
		self.logger.debug("Monitor bailed")

# end classes


# begin config

_config = None

def load_config():
	logger.debug("loading config")
	global _config
	import pickle
	try:
		fn = os.path.expanduser(n_config)
		f = open(fn,"r")
		_config = pickle.load(f)
		f.close()
		logger.debug("loaded config successfully")
	except IOError, e: _config = None
	except EOFError,e: _config = None
	if _config is None:
		logger.debug("could not load config, using defaults")
		_config = dict() ; s = "super_dynamic_mode"
		_config[s] = dict()
		_config[s]["enabled"] = False
		_config[s]["top_choices"] = 5
		_config[s]["upcoming_songs"] = 3
		_config[s]["choice_mechanism"] = LAST_SONG_IN_PLAYLIST
		_config[s]["insert_position"] = LAST_SONG_IN_PLAYLIST
		_config[s]["tempo_spectrum_weights"] = (1.0,1.0)
		
	

def save_config():
	logger.debug("saving config")
	global _config
	fn = os.path.expanduser(n_config)
	os.umask(077)
	try:
		f = open(fn,"w")
		import pickle
		pickle.dump(_config,f)
		f.close()
	except:
		logger.exception("could not save config, continuing")

def get_config():
	global _config
	return _config

notify_config_changed_handler = None
def notify_config_changed():
# 	logger.debug("Configuration changed, performing appropriate actions")
	if notify_config_changed_handler: notify_config_changed_handler()

# end config


# public interface

class PublicInterface:
	def __init__(self,collection_expert_instance):
		for a in ["get_similar_songs","get_current_song","get_total_songs","get_remaining_songs","get_priority_queue_count","get_song"]:
			setattr(self,a,getattr(collection_expert_instance,a))
		self.get_config = get_config
		self.notify_config_changed = notify_config_changed

#end public interface


#UI's
custom_menu_items = ["Find songs similar to selection","Analyze selection now","Set up Auto DJ mode"]
	
def enable_ui():
	logger.debug("enabling ui")
	for a in custom_menu_items:
		run("dcop","amarok","script","addCustomMenuItem","Smart DJ",a)
	
def disable_ui():
	logger.debug("disabling ui")
	for a in custom_menu_items:
		run("dcop","amarok","script","removeCustomMenuItem","Smart DJ",a)
	logger.debug("ui disabled")
	
def initialize_ui(public_interface):
	songanalysisui.initialize(public_interface)
	
#end UI's

# main function



class SignalQueue(Queue):
	def __init__(self):
		Queue.__init__(self)
	
	def handler(self,signal,frame):
		"""The installable signal handler"""
		logger.info( "received signal %s"%signal)
		self.put(signal)
		
	def trap(self,signal):
		from signal import signal as sigaction
		sigaction(signal,self.handler)


def _actual_main():

	
	pid = os.fork()
	if (pid): # the child, we go on as normal
		logger.debug("Parent process: forked.  PID: %s",os.getpid())
		signal_queue = SignalQueue()
		for sig in [signal.SIGINT,signal.SIGTERM]: signal_queue.trap(sig)
		while True:
			try: sig = signal_queue.get(block=True,timeout=1)
			except Empty:
				pid,status = os.waitpid(pid,os.WNOHANG)
				if pid:
					if os.WCOREDUMP(status):
						logger.debug("Child core dumped")
					elif os.WIFSIGNALED(status):
						logger.debug("Child got an unhandled signal %s",os.WTERMSIG(status))
					elif os.WIFEXITED(status):
						logger.debug("Child returned with return value %s",os.WEXITSTATUS(status))
					else:
						logger.debug("Child died with status %s",status)
					break
				continue
			logger.debug("Parent process got signal %s, going down and letting child notice on its own",sig)
# 			os.kill(pid,sig)
			break
		logger.debug("Parent process going down")
# 		while signal_queue.qsize() < 1:
# 			
# 			os.wait()
	else: # the parent, we exit now
		logger.debug("Child process: forked.  PID: %s",os.getpid())
	
		logger.info("starting up")
		load_config()
		
		logger.info("UI available: %s"%ui_available)
		
		collection_expert = CollectionExpert()
		playlist_expert = PlaylistExpert()
		
		if ui_available:
			public_interface = PublicInterface(collection_expert)
			initialize_ui(public_interface)
			enable_ui()
			
		
	# 	expert.prepare_database() # for future things!
		collection_expert.purge_songs_not_analyzed_with_interface(get_available_songanalysis_interface()) #maybe this belongs in the BackgroundSongAnalyzer?
		analyzer = BackgroundSongAnalyzer(collection_expert)
		super_dynamic_mode_monitor = SuperDynamicModeMonitor(playlist_expert,collection_expert)
		monitor = AmarokSlaveMonitor(playlist_expert,collection_expert,super_dynamic_mode_monitor)
		
		# setting up a signal handler
		global notify_config_changed_handler
		def g():
	# 		logger.debug("Config change signal handler called")
			save_config()
			super_dynamic_mode_monitor.notify("config_changed")
		notify_config_changed_handler = g
		
		signal_queue = SignalQueue()
		for sig in [signal.SIGINT,signal.SIGTERM]: signal_queue.trap(sig)
		
		logger.debug("starting analyzer")
		analyzer.start()
		logger.debug("starting Auto DJ monitor")
		super_dynamic_mode_monitor.start()
		logger.debug("starting monitor")
		monitor.start()
		
		logger.info("started")
		
		while analyzer.isAlive() and monitor.isAlive() and super_dynamic_mode_monitor.isAlive() and signal_queue.qsize() < 1: time.sleep(1)
	# 	logger.debug("forking to avoid the SIGKILL")
		
		
	# 	createDaemon()
	# 			
		if ui_available: disable_ui()
		
		logger.debug("ending monitor")
		monitor.stop(async=True)
		logger.debug("ending Auto DJ monitor")
		super_dynamic_mode_monitor.stop(async=True)
		logger.debug("ending analyzer")
		analyzer.stop(async=True)
		logger.debug("waiting for monitor")
		monitor.stop()
		logger.debug("waiting for Auto DJ monitor")
		super_dynamic_mode_monitor.stop()
		logger.debug("waiting for analyzer")
		analyzer.stop()
		
		
		save_config()
		
		logger.info("ended")
	
	
def main():
	
	setup_logging()
	all_okay = True
	try: _actual_main()
# 	except SystemExit,e:
# 		if e == 0: sys.exit(0)
# 		else: raise
	except:
		all_okay = False
		logger.exception("An error occurred while starting up.  PID: %s",os.getpid())
		say_sorry("An error took place while starting up the Smart DJ plug-in.  This is not your fault, but a bug in the Smart DJ plug-in.  Please report the contents of the file %s to the Smart DJ plug-in developers." %os.path.expanduser(n_logfile))
	
	finish_logging()
	if not all_okay: sys.exit(2)
	
	
if __name__ == "__main__":
	main()